<?php
/** Clasa publicata sub licenta GPL si adaptata pentru Amframework.
 * 
 * Licenta originala
 * -----------------
 * @package: AMFramework
 * @subpackage: Security
 * @class: InputFilter (PHP5-Strict with comments)
 * @project: PHP Input Filter
 * @date: 10-05-2005
 * @version: 1.2.2_php5
 * @author: Daniel Morris
 * @contributors: Gianpaolo Racca, Ghislain Picard, Marco Wandschneider, Chris Tobin and Andrew Eddie.
 * @copyright: Daniel Morris
 * @email: dan@rootcube.com
 * @license: GNU General Public License (GPL)
 * 
 * Versiunea originala descarcata de pe: http://www.phpclasses.org/inputfilter
 */
class Amfw_InputFilter extends Amfw_Object {
	private $tags_array;
	private $attr_array;
	private $tags_method;
	private $attr_method;
	private $xss_auto;
	
	private $tag_blacklist = array('applet', 'body', 'bgsound', 'base', 'basefont', 'embed', 'frame', 'frameset', 'head', 'html', 'id', 'iframe', 'ilayer', 'layer', 'link', 'meta', 'name', 'object', 'script', 'style', 'title', 'xml'); 
	private $attr_blacklist = array('action', 'background', 'codebase', 'dynsrc', 'lowsrc');
	
	// Public =================================================================
	
	/** Intoarce o instanta pre-initializata la obiectul de filtrare a inputului,
	 * evitand sa il creeze din nou daca acesta deja exista.
	 * 
	 * @access public
	 * @param Array $tags_array	Lista de taguri user-defined;
	 * @param Array $attr_array	Lista de atribute user-defined;
	 * @param int $tags_method	Comportamentul pentru tagurile user-defined:
	 * 							0 = accepta numai tagurile user-defined (default)
	 * 							1 = accepta orice mai putin tagurile user-defined
	 * @param int $attr_method	Comportamentul pentru atributele user-defined:
	 * 							0 = accepta numai atrib user-defined (default)
	 * 							1 = accepta orice mai putin atrib user-defined
	 * @param int $xss_auto		0 = curata automat numai ce e esential
	 * 							1 = curata taguri / atribute blacklisted
	 * @return Amfw_InputFilter
	 */
	public static function &getInstance()
	{
		static $if;	// InputFilter

		if(!isset($if)): $if = new Amfw_InputFilter(); endif;

		return $if;
	}
	
	/**
	 * @access public
	 * @param Array $tags_array	Lista de taguri user-defined;
	 * @param Array $attr_array	Lista de atribute user-defined;
	 * @param int $tags_method	Comportamentul pentru tagurile user-defined:
	 * 							0 = accepta numai tagurile user-defined (default)
	 * 							1 = accepta orice mai putin tagurile user-defined
	 * @param int $attr_method	Comportamentul pentru atributele user-defined:
	 * 							0 = accepta numai atrib user-defined (default)
	 * 							1 = accepta orice mai putin atrib user-defined
	 * @param int $xss_auto		0 = curata automat numai ce e esential
	 * 							1 = curata taguri / atribute blacklisted
	 */
	public function init(	$tags_array = array(),
							$attr_array = array(), 
							$tags_method = 0, 
							$attr_method = 0, 
							$xss_auto = 1
						) 
	{
		// Arrayurile trebuie sa aiba elementele in lowercase
        $tags_array = array_map('strtolower', (array) $tags_array);
        $attr_array = array_map('strtolower', (array) $attr_array);
		
		// Initializam proprietatile obiectului cu parametri primiti
		$this->tags_array = $tags_array;
		$this->attr_array = $attr_array;
		$this->tags_method = $tags_method;
		$this->attr_method = $attr_method;
		$this->xss_auto = $xss_auto;
	}
	
	/** 
	  * Method to be called by another php script. Processes for XSS and specified bad code.
	  * @access public
	  * @param Mixed $source - input string/array-of-string to be 'cleaned'
	  * @return String $source - 'cleaned' version of input parameter
	  */
	public function process( $source ) {
		// Clean all elements in this array
		if(is_array($source)):
			foreach($source as &$value) {
				// Filter element for XSS and other 'bad' code etc;
				if(is_string($value)): $value = $this->_remove($this->_decode($value)); endif;
			}
			return $source;
		elseif(is_string($source)):
			return $this->_remove($this->_decode($source));
		else: return $source; // Return parameter as given
		endif; 
	}
	
	
	// Private ================================================================
	private function _remove( $source ) {
		// Iteration which provides nested-tag protection
		while($source != $this->_tagsfilter($source))
			$source = $this->_tagsfilter($source);
		return $source;
	}
	
	/** Metoda interna pentru eliminarea tagurilor nesigure
	 * @param String $source - stringul ce trebuie `curatat`
	 * @return String $source - versiunea `curata` a stringului
	 */
	private function _tagsfilter($source) //------------------------------------------------------- 
	{
		// Filter pass setup;
		$pre_tag = null;
		$post_tag = $source;

		// Find initial tag's position
		$tag_open_start = strpos($source, '<');
		
		// Iterate through string until no tags left
		while($tag_open_start !== false) {
			// Process tags iteratively
			$pre_tag .= substr($post_tag, 0, $tag_open_start);
			$post_tag = substr($post_tag, $tag_open_start);
			$from_tag_open = substr($post_tag, 1);

			// End of tag
			$tag_open_end = strpos($from_tag_open, '>');
			if ($tag_open_end === false) break;

			// Next start of tag (for nested tag assessment)
			$tag_open_nested = strpos($from_tag_open, '<');

			if (($tag_open_nested !== false) && ($tag_open_nested < $tag_open_end)) 
			{
				$pre_tag .= substr($post_tag, 0, ($tag_open_nested + 1));
				$post_tag = substr($post_tag, ($tag_open_nested + 1));
				$tag_open_start = strpos($post_tag, '<');
				continue;
			} 

			$tag_open_nested = (strpos($from_tag_open, '<') + $tag_open_start + 1);
			$current_tag = substr($from_tag_open, 0, $tag_open_end);
			$tag_length = strlen($current_tag);
			if (!$tag_open_end) 
			{
				$pre_tag .= $post_tag;
				$tag_open_start = strpos($post_tag, '<');			
			}
			
			// Iterate through tag finding attribute pairs - setup
			$tag_left = $current_tag;
			$attr_set = array();
			$current_space = strpos($tag_left, ' ');
			
			// Is end tag
			if (substr($current_tag, 0, 1) == "/") {
				$is_close_tag = true;
				list($tag_name) = explode(' ', $current_tag);
				$tag_name = substr($tag_name, 1);
			} 
			// Is start tag
			else  
			{
				$is_close_tag = false;
				list($tag_name) = explode(' ', $current_tag);
			}
			
			// Excludes all "non-regular" tagnames OR no tagname OR 
			// remove if xss_auto is on and tag is blacklisted
			if 	((!preg_match("/^[a-z][a-z0-9]*$/i",$tag_name)) || (!$tag_name) || 
				((in_array(strtolower($tag_name), $this->tag_blacklist)) && ($this->xss_auto))) 
			{
				$post_tag = substr($post_tag, ($tag_length + 2));
				$tag_open_start = strpos($post_tag, '<');
				// Don't append this tag
				continue;
			}
			// This while is needed to support attribute values with spaces in
			while ($current_space !== false) 
			{
				$from_space = substr($tag_left, ($current_space+1));
				$next_space = strpos($from_space, ' ');
				$open_quotes = strpos($from_space, '"');
				$close_quotes = strpos(substr($from_space, ($open_quotes+1)), '"') + $open_quotes + 1;

				// Another equals exists
				if (strpos($from_space, '=') !== false) 
				{
					// Opening and closing quotes exists
					if (($open_quotes !== false) && (strpos(substr($from_space, ($open_quotes + 1)), '"') !== false))
						$attr = substr($from_space, 0, ($close_quotes+1));
					// One or neither exist
					else $attr = substr($from_space, 0, $next_space);
				// No more equals exist
				} else $attr = substr($from_space, 0, $next_space);
				// Last attr pair
				if (!$attr) $attr = $from_space;
				// Add to attribute pairs array
				$attr_set[] = $attr;
				// Next inc
				$tag_left = substr($from_space, strlen($attr));
				$current_space = strpos($tag_left, ' ');
			}
			// Appears in array specified by user
			$tag_found = in_array(strtolower($tag_name), $this->tags_array);			
			// Remove this tag on condition
			if ((!$tag_found && $this->tags_method) || ($tag_found && !$this->tags_method)) 
			{
				// Reconstruct tag with allowed attributes
				if (!$is_close_tag) 
				{
					$attr_set = $this->_attrfilter($attr_set);
					$pre_tag .= '<' . $tag_name;
					for ($i = 0; $i < count($attr_set); $i++)
						$pre_tag .= ' ' . $attr_set[$i];
					// Reformat single tags to XHTML
					if (strpos($from_tag_open, "</" . $tag_name)) $pre_tag .= '>';
					else $pre_tag .= ' />';
				// Just the tagname
			    } else $pre_tag .= '</' . $tag_name . '>';
			}
			// Find next tag's start
			$post_tag = substr($post_tag, ($tag_length + 2));
			$tag_open_start = strpos($post_tag, '<');			
		}
		// Append any code after end of tags
		$pre_tag .= $post_tag;
		return $pre_tag;
	}
	
	/** Metoda interna pentru eliminarea atributelor nesigure
	 * @param Array $attr_set
	 * @return Array $new_set
	 */
	private function _attrfilter( $attr_set ) //---------------------------------------------------
	{ 
		$new_set = array();

		// Process attributes
		for ($i = 0; $i <count($attr_set); $i++) 
		{
			// Skip blank spaces in tag
			if (!$attr_set[$i]) continue;

			// Split into attr name and value
			$attr_sub_set = explode('=', trim($attr_set[$i]));
			list($attr_sub_set[0]) = explode(' ', $attr_sub_set[0]);

			// Removes all "non-regular" attr names AND also attr blacklisted
			if ((!@eregi("^[a-z]*$",$attr_sub_set[0])) || (($this->xss_auto) && 
				((in_array(strtolower($attr_sub_set[0]), $this->attr_blacklist)) || 
				(substr($attr_sub_set[0], 0, 2) == 'on')))) 
				continue;

			// XSS attribute value filtering
			if ($attr_sub_set[1]) 
			{
				// Strips unicode, hex, etc
				$attr_sub_set[1] = str_replace('&#', '', $attr_sub_set[1]);
				// Strip normal newline within attr value
				$attr_sub_set[1] = preg_replace('/\s+/', '', $attr_sub_set[1]);
				// Strip double quotes
				$attr_sub_set[1] = str_replace('"', '', $attr_sub_set[1]);
				// Convert single quotes from either side to doubles
				// (Single quotes shouldn't be used to pad attr value)
				if ((substr($attr_sub_set[1], 0, 1) == "'") && (substr($attr_sub_set[1], (strlen($attr_sub_set[1]) - 1), 1) == "'"))
					$attr_sub_set[1] = substr($attr_sub_set[1], 1, (strlen($attr_sub_set[1]) - 2));
				// Strip slashes
				$attr_sub_set[1] = stripslashes($attr_sub_set[1]);
			}
			// Auto strip attr's with "javascript:
			if (	((strpos(strtolower($attr_sub_set[1]), 'expression') !== false) &&	(strtolower($attr_sub_set[0]) == 'style')) ||
					(strpos(strtolower($attr_sub_set[1]), 'javascript:') !== false) ||
					(strpos(strtolower($attr_sub_set[1]), 'behaviour:') !== false) ||
					(strpos(strtolower($attr_sub_set[1]), 'vbscript:') !== false) ||
					(strpos(strtolower($attr_sub_set[1]), 'mocha:') !== false) ||
					(strpos(strtolower($attr_sub_set[1]), 'livescript:') !== false) 
			) continue;

			// If matches user defined array
			$attr_found = in_array(strtolower($attr_sub_set[0]), $this->attr_array);
			
			// Keep this attr on condition
			if ((!$attr_found && $this->attr_method) || ($attr_found && !$this->attr_method)) 
			{
				// Attr has value
				if ($attr_sub_set[1]) $new_set[] = $attr_sub_set[0] . '="' . $attr_sub_set[1] . '"';
				// Attr has decimal zero as value
				else if ($attr_sub_set[1] == "0") $new_set[] = $attr_sub_set[0] . '="0"';
				// Reformat single attributes to XHTML
				else $new_set[] = $attr_sub_set[0] . '="' . $attr_sub_set[0] . '"';
			}
		}
		return $new_set;
	}
	
	private function _decode($source) {
		// URL decode
		$source = html_entity_decode($source, ENT_QUOTES, "ISO-8859-1");
		// Convert decimal
		$source = preg_replace('/&#(\d+);/me',"chr(\\1)", $source);				// Decimal notation
		// Convert hex
		$source = preg_replace('/&#x([a-f0-9]+);/mei',"chr(0x\\1)", $source);	// Hex notation
		return $source;
	}
}